package de.zainodis.commons.utils;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import de.zainodis.commons.LogCat;
import de.zainodis.commons.exception.ReflectionException;

public final class ReflectionUtils {

   private static final String TAG = "ReflectionUtils";

   /**
    * Creates an instance of the provided type.
    * 
    * @param type
    *           the type to be instantiated
    * @return an instance of the provided type
    * @throws ReflectionException
    *            if either instantiation failed or instantiation of the given
    *            type has been denied due to access control
    */
   public static <T> T newInstance(Class<T> type) throws ReflectionException {
	 try {
	    return type.newInstance();

	 } catch (InstantiationException e) {
	    String errorMessage = "Instantiation of type " + type.getName() + " failed";
	    throw new ReflectionException(errorMessage, e);
	 } catch (IllegalAccessException e) {
	    String errorMessage = "Access for instantiation of type " + type.getName() + " was denied";
	    throw new ReflectionException(errorMessage, e);
	 }
   }

   public static Object newInstance(Class<?> type, Class<?>[] constructorParameters,
	    Object[] constructorArguments) throws ReflectionException {

	 try {
	    Constructor<?> constructor = type.getConstructor(constructorParameters);
	    return constructor.newInstance(constructorArguments);

	 } catch (InstantiationException e) {
	    String errorMessage = "Instantiation of type " + type.getName() + " failed";
	    throw new ReflectionException(errorMessage, e);
	 } catch (IllegalAccessException e) {
	    String errorMessage = "Access for instantiation of type " + type.getName() + " was denied";
	    throw new ReflectionException(errorMessage, e);
	 } catch (IllegalArgumentException e) {
	    String errorMessage = "There is no compatible constructor for " + type.getCanonicalName()
			+ " matching the provided list of parameters and arguments";
	    throw new ReflectionException(errorMessage, e);
	 } catch (InvocationTargetException e) {
	    String errorMessage = "An exception has been thrown in the constructor of "
			+ type.getCanonicalName();
	    throw new ReflectionException(errorMessage, e);
	 } catch (SecurityException e) {
	    String errorMessage = "There constructor of " + type.getCanonicalName()
			+ " may not be invoked due to access restrictions";
	    throw new ReflectionException(errorMessage, e);
	 } catch (NoSuchMethodException e) {
	    String errorMessage = "No constructor found for " + type.getCanonicalName()
			+ " which matched the given list of constructor arguments.";
	    throw new ReflectionException(errorMessage, e);
	 }
   }

   /**
    * Convert the given type name to it's corresponding class. Fails if no
    * matching class could be found. it is required to provide the canonical
    * class name.
    * 
    * @param typeName
    *           name of the class whose type should be obtained
    * @return the matching class for the given type name
    * @throws ReflectionException
    *            thrown if no class could be found, that matches the given type
    *            name
    */
   public static Class<?> toType(String typeName) throws ReflectionException {

	 try {
	    return Class.forName(typeName);

	 } catch (ClassNotFoundException e) {
	    String errorMessage = "No matching class found for type " + typeName;
	    throw new ReflectionException(errorMessage, e);
	 }
   }

   /**
    * Converts the given type name to it's corresponding type and creates an
    * instance of it. Fails if no matching class could be found for
    * instantiating the given type name or if no instance could be created.
    * 
    * @param typeName
    *           the name of the to be instantiated type
    * @return an instance of the given type
    * @throws ReflectionException
    *            for more information as to when this is thrown, see
    *            {@link #newInstance(Class)} and {@link #toType(String)}
    */
   public static Object getInstance(String typeName) throws ReflectionException {
	 Class<?> type = toType(typeName);
	 return newInstance(type);
   }

   /**
    * Returns an instance of the given type and assures, that the returned
    * instance is of the passed type.
    * 
    * @param typeName
    *           the fully qualified name of the type that is to be instantiated
    * @param expectedType
    *           the expected type of the instantiated type - this may either be
    *           the exact type or a superclass of the expected type
    * @return an instance of the requested type
    * @throws ReflectionException
    *            for more information as to when this is thrown, see
    *            {@link #newInstance(Class)} and {@link #toType(String)} is also
    *            thrown if the expected type does not match the actual type
    */
   // TODO consider making generic
   public static Object getInstance(String typeName, Class<?> expectedType)
	    throws ReflectionException {
	 Class<?> type = toType(typeName);
	 if (expectedType.isAssignableFrom(type)) {
	    return newInstance(type);
	 }
	 throw new ReflectionException(typeName + " is is not (a subtype of) "
		  + expectedType.getCanonicalName());
   }

   /**
    * Invokes the given method on the provided object with the given arguments.
    * If the provided object is null, the method is attempted to be invoked as a
    * static method.
    * 
    * @param method
    *           the method that is invoked
    * @param invokeOn
    *           the object on which the method is invoked; or null, if a static
    *           method is to be invoked.
    * @param arguments
    *           arguments that are required to invoke the given method. Null is
    *           automatically cast to an Object array. Beware though, that this
    *           will only succeed if the method does not define any parameters
    * @return an Object containing the result of the invoke; null, if the method
    *         has the return type void
    * @throws ReflectionException
    *            is thrown if an attempt to invoke the method resulted in an
    *            {@link IllegalArgumentException},
    *            {@link IllegalAccessException} or
    *            {@link InvocationTargetException}
    * @throws IllegalArgumentException
    *            is thrown, if one of the provided arguments is null
    * 
    */
   public static Object invoke(Method method, Object invokeOn, Object... arguments)
	    throws ReflectionException {
	 try {
	    if (method == null) {
		  throw new IllegalArgumentException("Method to be invoked must not be null.");
	    }

	    // Avoid null arguments
	    arguments = arguments != null ? arguments : new Object[0];
	    return method.invoke(invokeOn, arguments);

	 } catch (IllegalAccessException e) {
	    throw new ReflectionException("The method " + method.getName()
			+ " may not be invoked - acces was denied", e);
	 } catch (InvocationTargetException e) {
	    throw new ReflectionException("An exception was thrown by the invoked method.", e);
	 } catch (Throwable e) {
	    throw new ReflectionException("Failed to invoke method " + method.getName(), e);
	 }
   }

   /**
    * Invokes the specified method with the given arguments.
    * 
    * @param targetClass
    *           FQDN of the class which contains the method to be invoked.
    * @param targetMethod
    *           name of the static, public method of the given target class,
    *           that should be invoked.
    * @param arguments
    *           only one-argument methods are supported. The argument will
    *           automatically be converted using {@link TypeConverter}, based on
    *           the method's parameter's type.
    * @return an Object containing the result of the invoke or null, if the
    *         method has the return type void
    * @throws ReflectionException
    *            on error.
    */
   public static Object invoke(String targetClass, String targetMethod, Object argument)
	    throws ReflectionException {
	 try {
	    /*
	     * Since we don't know the methods parameter types, we need to search
	     * by name.
	     */
	    Method[] methods = Class.forName(targetClass).getMethods();
	    for (Method method : methods) {
		  if (method.getName().equals(targetMethod)) {
			Class[] parameterTypes = method.getParameterTypes();
			// Ensure that the method has only one parameter
			if (parameterTypes.length != 1) {
			   throw new ReflectionException(
				    "Only one-parameter methods are supported, failed to invoke "
						+ targetMethod);
			}
			// We found the correct method, invoke it!
			return method.invoke(null,
				 TypeConverter.convertTo(String.valueOf(argument), parameterTypes[0]));
		  }
	    }

	 } catch (ClassNotFoundException e) {
	    throw new ReflectionException("Failed to find the class " + targetClass, e);
	 } catch (IllegalArgumentException e) {
	    throw new ReflectionException("Failed to invoke method " + targetMethod, e);
	 } catch (IllegalAccessException e) {
	    throw new ReflectionException("Failed to access the method " + targetMethod, e);
	 } catch (InvocationTargetException e) {
	    throw new ReflectionException("Error while invoking the method " + targetMethod, e);
	 }
	 throw new ReflectionException("Method does not exist " + targetMethod,
		  new NoSuchMethodException());
   }

   /**
    * Returns the specified method (specified by its name and parameters) which
    * is declared in the given class.
    * 
    * @param type
    *           the class to which the method belongs
    * @param methodName
    *           the name of the method
    * @param parameters
    *           the parameters that uniquely identify the method's signature
    * @return the matching method
    * @throws ReflectionException
    *            if either a {@link SecurityException} occurred when trying to
    *            retrieve the method or when the method was not found (either
    *            because it does not exist at all, or because the parameter list
    *            was incorrect
    */
   public static Method getMethod(Class<?> type, String methodName, Class<?>... parameters)
	    throws ReflectionException {
	 try {
	    return type.getMethod(methodName, parameters);

	 } catch (SecurityException e) {
	    String errorMessage = "Access denied: Could not access method " + methodName
			+ " in class " + type.getCanonicalName();
	    throw new ReflectionException(errorMessage, e);
	 } catch (NoSuchMethodException e) {
	    String errorMessage = "NoSuchMethodException: Could not find method " + methodName
			+ " in class " + type.getCanonicalName()
			+ ", tip: ensure the list of passed parameters is correct";
	    throw new ReflectionException(errorMessage, e);
	 }
   }

   /**
    * Returns the specified Field, that is declared in the class of the given
    * type.
    * 
    * @param type
    *           the class to which the Field of the given name belongs
    * @param fieldName
    *           the name of the Field that is to be returned
    * @return the Field whose name and class has been provided>
    * @throws ReflectionException
    *            if either the Field could not be accessed due to access
    *            restrictions or the Field could not be found or the provided
    *            type could not be found
    */
   public static Field getField(Class<?> type, String fieldName) throws ReflectionException {
	 try {
	    return type.getField(fieldName);

	 } catch (SecurityException e) {
	    String errorMessage = "Access denied: Could not access field " + fieldName + " in class "
			+ type;
	    throw new ReflectionException(errorMessage, e);
	 } catch (NoSuchFieldException e) {
	    String errorMessage = "NoSuchFieldException: Could not find field " + fieldName
			+ " in class " + type;
	    throw new ReflectionException(errorMessage, e);
	 }
   }

   public static Object getFieldValue(Object fieldOwner, String fieldName)
	    throws ReflectionException {
	 Class<?> type = fieldOwner.getClass();

	 try {
	    Field field = type.getField(fieldName);
	    return field.get(fieldOwner);

	 } catch (IllegalAccessException e) {
	    String errorMessage = "Failed to get value of field " + fieldName
			+ ", the field may not be accessed (tip: check modifiers)";
	    throw new ReflectionException(errorMessage, e);
	 } catch (NoSuchFieldException e) {
	    String errorMessage = "Could not find field " + fieldName + " in class "
			+ type.getCanonicalName();
	    throw new ReflectionException(errorMessage, e);
	 }
   }

   /**
    * Attempts to find a static field in the given owner class, whose name
    * matches the given field name and returns it's value. Make sure the field
    * you wish to access is declared as public and static.
    * 
    * @param owner
    *           the class which owns the field.
    * @param fieldName
    *           the name of the static field.
    * @return the value of the static field that matches the given field name.
    */
   public static Object getStaticFieldValue(Class<?> owner, String fieldName)
	    throws ReflectionException {

	 try {
	    Field field = owner.getField(fieldName);
	    return field.get(null);

	 } catch (IllegalAccessException e) {
	    String errorMessage = "Failed to get value of field " + fieldName
			+ ", the field may not be accessed (tip: check modifiers)";
	    throw new ReflectionException(errorMessage, e);
	 } catch (NoSuchFieldException e) {
	    String errorMessage = "Could not find field " + fieldName + " in class " + owner;
	    throw new ReflectionException(errorMessage, e);
	 }
   }

   /**
    * Sets the given value for the given Field. In case of static Fields, the
    * fieldOwner argument will be ignored.
    * 
    * @param field
    *           the Field whose value will be set to the given value
    * @param fieldOwner
    *           an instance of the class in which the Field is declared or null
    *           if the Field is static
    * @param value
    *           the value which will be set for the given Field
    * @throws ReflectionException
    *            if the fieldOwner is not compatible with the declaring class
    */
   public static void setFieldValue(Field field, Object fieldOwner, Object value)
	    throws ReflectionException {
	 try {
	    field.set(fieldOwner, value);

	 } catch (IllegalArgumentException e) {
	    String errorMessage = "Failed to set value " + value + " for field " + field.getName()
			+ " field owner is not compatible with the declaring class";
	    LogCat.e(TAG, errorMessage);
	    throw new ReflectionException(errorMessage, e);

	 } catch (IllegalAccessException e) {
	    String errorMessage = "Failed to set value " + value + " for field " + field.getName()
			+ " the field may not be accessed (tip: check modifiers)";
	    LogCat.e(TAG, errorMessage);
	    throw new ReflectionException(errorMessage, e);
	 }
   }

   /**
    * Obtains the class using the given className, then creates an instance of
    * {@link Field} using the given field name (must be the name of a public,
    * static field) and finally the given string is converted to a value whose
    * type matches the target field's type. The target field is then updated
    * with the converted value.
    * 
    * @param className
    *           FQDN of the target class, whose field's value should be updated
    * @param fieldName
    *           the name of the field whose value should be updated. The field
    *           must exist in the given class and must be public and static.
    * @param value
    *           the value which will be set for the given Field. The value must
    *           be provided as a string and will be auto-converted to the target
    *           field's type using the {@link TypeConverter}.
    * @throws ReflectionException
    *            on error.
    */
   public static void setFieldValue(String className, String fieldName, String value)
	    throws ReflectionException {
	 try {
	    // Create field instance, only public static fields are supported
	    Field field = ReflectionUtils.getField(Class.forName(className), fieldName);
	    // Convert the given value
	    Object newValue = TypeConverter.convertTo(value, field.getType());
	    // Update the field's value
	    setFieldValue(field, null, newValue);

	 } catch (Throwable e) {
	    throw new ReflectionException(String.format("Failed to value of field %s to %s",
			fieldName, value), e);

	 }
   }
}
